/**
 * Copyright(c) 2014 Xunton Solutions
 *
 * History:
 *   15-1-18 下午8:59 Created by dwzhang
 */
package com.jade.modules.cms.impl;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ejb.TransactionAttribute;
import javax.inject.Inject;
import javax.inject.Named;

import com.jade.framework.base.criterion.Criteria;
import com.jade.framework.base.lang.StatusException;
import com.jade.framework.base.pattern.pipeline.Pipeline;
import com.jade.framework.base.text.VelocityUtils;
import com.jade.framework.base.util.DatetimeUtils;
import com.jade.framework.base.util.PagingList;
import com.jade.framework.base.util.Parameters;
import com.jade.framework.base.util.SimpleParameters;
import com.jade.framework.base.vfile.VFileInfo;
import com.jade.framework.base.vfile.VFileUtils;
import com.jade.journal.Journal;
import com.jade.journal.JournalUtils;
import com.jade.modules.cms.*;
import com.jade.modules.cms.pipeline.ContentContext;
import com.jade.modules.template.Template;
import com.jade.modules.template.TemplateManager;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.annotation.Transactional;

/**
 * 内容管理器实现。
 *
 * @author <a href="mailto:dwzhang@xunton.com">dwzhang</a> 
 * @version 1.0 15-1-18 下午8:59
 */
//@Named ("module_cmsContentManager")
public class ContentManagerImpl implements ContentManager
{
    /**
     * "before"类管道如果需要阻止相应的操作如create、cancel、update、remove等，请抛出CmsException
     * 否则请阻止一切异常抛出，包括运行时异常。其它类管道无此限制。
     */
    public static final String PIPELINE_BEFORE_CREATE = "before_create";
    public static final String PIPELINE_AFTER_CREATE = "after_create";
    public static final String PIPELINE_BEFORE_SUBMIT = "before_submit";
    public static final String PIPELINE_AFTER_SUBMIT = "after_submit";
    public static final String PIPELINE_BEFORE_UPDATE = "before_update";
    public static final String PIPELINE_AFTER_UPDATE = "after_update";
    public static final String PIPELINE_BEFORE_AUDIT = "before_audit";
    public static final String PIPELINE_AFTER_AUDIT = "after_audit";
    public static final String PIPELINE_BEFORE_PUBLISH = "before_publish";
    public static final String PIPELINE_AFTER_PUBLISH = "after_publish";
    public static final String PIPELINE_BEFORE_WITHDRAW = "before_withdraw";
    public static final String PIPELINE_AFTER_WITHDRAW = "after_withdraw";
    public static final String PIPELINE_BEFORE_REOPEN = "before_reopen";
    public static final String PIPELINE_AFTER_REOPEN = "after_reopen";
    public static final String PIPELINE_BEFORE_REMOVE = "before_remove";
    public static final String PIPELINE_AFTER_REMOVE = "after_remove";

    private CategoryManager categoryManager;
    private TemplateManager templateManager;
    private ContentDao contentDao;
    protected Log log = LogFactory.getLog(ContentManagerImpl.class);
    protected Map<String, Pipeline<ContentContext, CmsException>> pipelineMap;
    protected String storeFileType = CmsConstants.CONTENT_FILE_TYPE;

//    @Inject
    public void setContentDao(@Named ("module_cmsContentDao") ContentDao contentDao)
    {
        this.contentDao = contentDao;
    }

//    @Inject
    public void setCategoryManager(@Named ("module_cmsCategoryManager") CategoryManager categoryManager)
    {
        this.categoryManager = categoryManager;
    }

//    @Inject
    public void setTemplateManager(@Named ("module_templateManager") TemplateManager templateManager)
    {
        this.templateManager = templateManager;
    }

    public void setPipelineMap(Map<String, Pipeline<ContentContext, CmsException>> pipelineMap)
    {
        this.pipelineMap = pipelineMap;
    }

    public void setStoreFileType(String storeFileType)
    {
        this.storeFileType = storeFileType;
    }

    protected ContentOpRecord createRecord(String operatorType, String operator, String op, long contentId,
                                           String remark)
    {
        ContentOpRecord record = new ContentOpRecord();
        Date now = new Date();
        record.setContentId(contentId);
        record.setResult(true);
        record.setExecuteDate(now);
        record.setOperation(op);
        record.setOperatorType(operatorType);
        record.setOperator(operator);
        record.setRemark(remark);
        return record;
    }

    protected Pipeline<ContentContext, CmsException> getPipeline(String name)
    {
        return pipelineMap == null ? null : pipelineMap.get(name);
    }

    protected void beforePipeline(String name, Content content)
            throws CmsException
    {
        Pipeline<ContentContext, CmsException> pipeline = getPipeline(name);
        if (pipeline != null) {
            pipeline.handle(new ContentContext(content));
        }
    }

    protected void afterPipeline(String name, Content content)
    {
        try {
            Pipeline<ContentContext, CmsException> pipeline = getPipeline(name);
            if (pipeline != null) {
                pipeline.handle(new ContentContext(content));
            }
        }
        catch (Throwable t) {
            log.warn("[" + content.getId() + "]" + "Processing content by pipeline(" + name + ") error:", t);
        }
    }

    protected String buildLog(String prefix, String operatorType, String operator, Content content)
    {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append(":");
        sb.append("operatorType=").append(operatorType).append(",");
        sb.append("operator=").append(operator).append(",");
        sb.append("content=").append(content);
        return sb.toString();
    }

    protected void handleException(Throwable t, ContentOpRecord record)
            throws CmsException
    {
        CmsException exception;
        if (t instanceof CmsException) {
            exception = (CmsException) t;
        }
        else {
            if (t instanceof StatusException) {
                exception = new CmsException(((StatusException) t).getStatusCode(), t.getMessage(), t.getCause(),
                        ((StatusException) t).getArguments());
            }
            else {
                exception = new CmsException(CmsConstants.ERR_UNEXPECTED, t, t.getMessage());
            }
        }
        if (record != null) {
            record.setResult(false);
            record.setErrorCode(exception.getStatusCode());
            record.setErrorMessage(exception.getMessage());
        }
        throw exception;
    }

    Journal journal = JournalUtils.getJournal(ContentOpRecord.NAME);

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void create(String operatorType, String operator, Content content)
            throws CmsException
    {
        Date now = new Date();
        try {
            content.setCreationDate(now);
            content.setUpdateDate(now);
            beforePipeline(PIPELINE_BEFORE_CREATE, content);
            add(content);
            afterPipeline(PIPELINE_AFTER_CREATE, content);
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_CREATE, content.getId(),
                    null);
            journal.infoEntity(record);
        }
        catch (Throwable t) {
            handleException(t, null);
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void update(String operatorType, String operator, Content content)
            throws CmsException
    {
        ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_UPDATE, content.getId(), null);
        try {
            beforePipeline(PIPELINE_BEFORE_UPDATE, content);
            update(content);
            content = get(content.getId());
            afterPipeline(PIPELINE_AFTER_UPDATE, content);
        }
        catch (Throwable t) {
            handleException(t, record);
        }
        finally {
            journal.infoEntity(record);
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void submit(String operatorType, String operator, long[] contentIds, String remark)
            throws CmsException
    {
        for (long contentId : contentIds) {
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_SUBMIT, contentId, remark);
            try {
                // check content status
                Content content = get(contentId);
                if (content.getStatus() >= Content.STATUS_SUBMITTED) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_INVALID_STATUS, "invalid status");
                }
                beforePipeline(PIPELINE_BEFORE_SUBMIT, content);
                int count = update(content.getId(), Content.STATUS_SUBMITTED, new int[]{Content.STATUS_CREATED});
                if (count > 0) {
                    content.setStatus(Content.STATUS_SUBMITTED);
                    afterPipeline(PIPELINE_AFTER_SUBMIT, content);
                }
                else {
                    throw new CmsException(CmsConstants.ERR_CONTENT_EMPTY_UPDATE, "no record update");
                }
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void audit(String operatorType, String operator, long[] contentIds, boolean result, String remark)
            throws CmsException
    {
        for (long contentId : contentIds) {
            String op = result ? ContentOpRecord.OP_AUDIT : ContentOpRecord.OP_REFUSE;
            int status = result ? Content.STATUS_AUDITED : Content.STATUS_REFUSED;
            ContentOpRecord record = createRecord(operatorType, operator, op, contentId, remark);
            try {
                // check content status
                Content content = get(contentId);
                if (content.getStatus() != Content.STATUS_SUBMITTED) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_INVALID_STATUS, "invalid status");
                }
                beforePipeline(PIPELINE_BEFORE_AUDIT, content);
                int count = update(content.getId(), status, new int[]{Content.STATUS_SUBMITTED});
                if (count > 0) {
                    content.setStatus(status);
                    afterPipeline(PIPELINE_AFTER_AUDIT, content);
                }
                else {
                    throw new CmsException(CmsConstants.ERR_CONTENT_EMPTY_UPDATE, "no record update");
                }
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    protected String generateHtmlContent(Content content)
            throws Exception
    {
        String htmlContent = mergeContent(content);
        //生成html文件
        VFileInfo fileInfo = VFileUtils.uploadFile(storeFileType, CmsConstants.CONTENT_FILE_CT,
                content.getId() + ".html", htmlContent, CmsConstants.CONTENT_FILE_ENCODING);
        return fileInfo.getFileId();
    }

    protected long getTemplateId(long categoryId)
    {
        CmsCategory cmsCategory = categoryManager.get(categoryId, false);
        if (cmsCategory.getTemplateId() == 0 && cmsCategory.getParentId() > 0) {
            return getTemplateId(cmsCategory.getParentId());
        }
        else {
            return cmsCategory.getTemplateId();
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void publish(String operatorType, String operator, long[] contentIds, String remark)
            throws CmsException
    {
        Date now = new Date();
        for (long contentId : contentIds) {
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_PUBLISH, contentId,
                    remark);
            try {
                // check content status
                Content content = get(contentId);
                if (content.getStatus() != Content.STATUS_AUDITED) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_INVALID_STATUS, "invalid status");
                }
                beforePipeline(PIPELINE_BEFORE_PUBLISH, content);
                content.setPublishDate(now);
                String fileId = generateHtmlContent(content);
                Parameters parameters = new SimpleParameters();
                parameters.setInt("status", Content.STATUS_PUBLISHED);
                parameters.setString("htmlFile", fileId);
                parameters.setObject("publishDate", content.getPublishDate());
                int count = update(content.getId(), parameters, new int[]{Content.STATUS_AUDITED});
                if (count > 0) {
                    content.setHtmlFile(fileId);
                    content.setStatus(Content.STATUS_PUBLISHED);
                    afterPipeline(PIPELINE_AFTER_WITHDRAW, content);
                }
                else {
                    throw new CmsException(CmsConstants.ERR_CONTENT_EMPTY_UPDATE, "no record update");
                }
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void withdraw(String operatorType, String operator, long[] contentIds, String remark)
            throws CmsException
    {
        for (long contentId : contentIds) {
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_WITHDRAW, contentId,
                    remark);
            try {
                // check content status
                Content content = get(contentId);
                if (content.getStatus() != Content.STATUS_PUBLISHED) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_INVALID_STATUS, "invalid status");
                }
                beforePipeline(PIPELINE_BEFORE_WITHDRAW, content);
                int count = update(content.getId(), Content.STATUS_WITHDRAWED, new int[]{Content.STATUS_PUBLISHED});
                if (count > 0) {
                    content.setStatus(Content.STATUS_WITHDRAWED);
                    afterPipeline(PIPELINE_AFTER_WITHDRAW, content);
                }
                else {
                    throw new CmsException(CmsConstants.ERR_CONTENT_EMPTY_UPDATE, "no record update");
                }
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    protected void removeContentFile(Content content)
            throws IOException
    {
        if (StringUtils.isNotBlank(content.getHtmlFile())) {
            VFileUtils.removeFile(storeFileType, content.getHtmlFile());
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void reopen(String operatorType, String operator, long[] contentIds, String remark)
            throws CmsException
    {
        for (long contentId : contentIds) {
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_REOPEN, contentId, remark);
            try {
                // check content status
                Content content = get(contentId);
                // check content status
                if (content.getStatus() <= Content.STATUS_PUBLISHED) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_INVALID_STATUS, "invalid status");
                }

                beforePipeline(PIPELINE_BEFORE_REOPEN, content);
                int count = update(content.getId(), Content.STATUS_CREATED,
                        new int[]{Content.STATUS_REFUSED, Content.STATUS_WITHDRAWED});
                if (count > 0) {
                    content.setStatus(Content.STATUS_CREATED);
                    afterPipeline(PIPELINE_AFTER_REOPEN, content);
                }
                else {
                    throw new CmsException(CmsConstants.ERR_CONTENT_EMPTY_UPDATE, "no record update");
                }
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void remove(String operatorType, String operator, long[] contentIds, String remark)
            throws CmsException
    {
        for (long contentId : contentIds) {
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_REMOVE, contentId, remark);
            try {
                // check content status
                Content content = get(contentId);
                if (content == null) {
                    continue;
                }
                if (content.getStatus() <= Content.STATUS_PUBLISHED && content.getStatus() > Content.STATUS_CREATED) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_INVALID_STATUS, "invalid status");
                }
                beforePipeline(PIPELINE_BEFORE_REMOVE, content);
                remove(content);
                afterPipeline(PIPELINE_AFTER_REMOVE, content);
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void updateFocus(String operatorType, String operator, long[] contentIds, boolean focus, String remark)
            throws CmsException
    {
        for (long contentId : contentIds) {
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_UPDATE_FOCUS, contentId,
                    remark);
            try {
                Content content = get(contentId);
                Parameters parameters = new SimpleParameters();
                parameters.setBoolean("focus", focus);
                int count = update(content.getId(), parameters, null);
                if (count <= 0) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_EMPTY_UPDATE, "no record update");
                }
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void updateSortCode(String operatorType, String operator, Map<Long, Integer> levelMap, String remark)
            throws CmsException
    {
        Set<Long> contentIds = levelMap.keySet();
        for (long contentId : contentIds) {
            ContentOpRecord record = createRecord(operatorType, operator, ContentOpRecord.OP_UPDATE_SORT_CODE,
                    contentId,
                    remark);
            try {
                Content content = get(contentId);
                Parameters parameters = new SimpleParameters();
                parameters.setInt("sortCode", levelMap.get(contentId));
                int count = update(content.getId(), parameters, null);
                if (count <= 0) {
                    throw new CmsException(CmsConstants.ERR_CONTENT_EMPTY_UPDATE, "no record update");
                }
            }
            catch (Throwable t) {
                handleException(t, record);
            }
            finally {
                journal.infoEntity(record);
            }
        }
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public long add(Content content)
            throws CmsException
    {
        contentDao.add(content);
        return content.getId();
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void update(Content content)
            throws CmsException
    {
        contentDao.update(content);
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public int update(long contentId, int status, int[] statuses)
            throws CmsException
    {
        Parameters parameters = new SimpleParameters();
        parameters.setInt("status", status);
        return contentDao.update(contentId, parameters, statuses);
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public int update(long contentId, Parameters parameters, int[] statuses)
            throws CmsException
    {
        return contentDao.update(contentId, parameters, statuses);
    }

    @TransactionAttribute
    @Transactional (rollbackFor = Throwable.class)
    public void remove(Content content)
            throws CmsException
    {
        contentDao.remove(content);
        //删除原来的html file
        try {
            removeContentFile(content);
        }
        catch (IOException e) {
            throw new CmsException(CmsConstants.ERR_IO, e.getMessage(), e);
        }
    }

    public String mergeContent(long id)
            throws CmsException
    {
        Content content = get(id);
        try {
            return mergeContent(content);
        }
        catch (Exception e) {
            throw new CmsException(CmsConstants.ERR_IO, e.getMessage(), e);
        }
    }

    protected String mergeContent(Content content)
            throws Exception
    {
        long templateId = content.getTemplateId();
        if (templateId == 0) {
            //获取category上面的。
            templateId = getTemplateId(content.getCategoryId());
        }
        Template template = null;
        if (templateId > 0) {
            template = templateManager.get(templateId);
        }
        //先处理details
        String details = content.getDetails();
        Parameters params = new SimpleParameters();
        params.setLong("id", content.getId());
        params.setString("name", content.getName());
        params.setString("title", content.getTitle());
        params.setString("author", content.getAuthor());
        params.setString("source", content.getSource());
        if (content.getCreationDate() != null) {
            params.setString("creationDate", DatetimeUtils.formatDate(content.getCreationDate(), "yyyy-MM-dd HH:mm"));
        }
        if (content.getPublishDate() != null) {
            params.setString("publishDate", DatetimeUtils.formatDate(content.getPublishDate(), "yyyy-MM-dd HH:mm"));
        }
        params.setString("summary", content.getRemark());
        String htmlContent = VelocityUtils.mergeContent(details, params);
        if (template != null) {
            String templateContent = template.getContent();
            params.setString("details", htmlContent);
            htmlContent = VelocityUtils.mergeContent(templateContent, params);
        }
        return htmlContent;
    }

    public Content get(long id)
    {
        return contentDao.get(id);
    }

    public List<Content> list(Criteria criteria)
    {
        return contentDao.list(criteria);
    }

    public PagingList<Content> query(Criteria criteria)
    {
        return contentDao.query(criteria);
    }
}
